Skip to content

摘要

你这段代码的 行为核心是“尾沿防抖(trailing debounce)”:快速连续切换 Tab 时,会合并为最后一次doFetch 执行。但它不仅是防抖,还叠加了两点工程化增强:

  1. 并发令牌 token:就算存在竞态(旧定时器没清干净、旧异步刚好触发),也会被 if (token !== lastFetchToken) return 硬性拦截,防止“过期任务”执行。
  2. “账户未就绪 → 一次性监听再补拉”:当 selectedAccount 为空时不立即拉取,而是注册一次性 watch,等账户就绪 自动补一次 fetchSpecs。这属于状态就绪门控,不是防抖/节流的范畴。

简单说:防抖的外壳 + 并发/状态的内功


与“常见防抖/节流”的对照

维度你的实现典型防抖(debounce)典型节流(throttle)
触发时机尾沿:最后一次触发后等待 100ms 执行可选前沿/尾沿,最常见是尾沿固定间隔内最多一次
多次快速触发只执行最后一次(清理上一个 timeout只执行最后一次(清理上一个 timeout可能执行多次,但频率受限
并发/竞态保护有令牌校验,避免过期任务乱入通常没有,需要自加通常没有,需要自加
依赖状态未就绪挂一次性监听后补拉不涉及不涉及
适用场景Tab 快速切换、确保只对最新选择拉取输入框搜索、窗口 resize 等滚动监听、窗口 resize 高频但需稳频

时间线直观示例

t=0ms: selectTab(A) -> 清前一个定时器 -> 设 timeout(T1, +100)
t=30ms: selectTab(B) -> 清 T1 -> 设 timeout(T2, +100)
t=60ms: selectTab(C) -> 清 T2 -> 设 timeout(T3, +100)
t=160ms: 只会执行 T3(C)的 doFetch

因为每次都会 clearTimeout只保留最后一个。而 token 让就算 T1/T2 因竞态触发也会被 “token 不匹配” 拦下。


为什么还需要 token

单靠 clearTimeout 理论上够用,但在真实环境下可能出现:

  • 浏览器/运行时调度导致已过期的定时回调仍被调用;
  • 代码后续异步链路中又触发了旧逻辑。

token 是“硬闸门”:每次选择加一,回调里先比对序号,不匹配就立即返回,确保“旧世界的回调进不来新世界”。


“账户就绪一次性监听”是加分项

selectedAccount 尚未就绪时:

  • 不白跑:不做无效拉取;
  • 不丢失:用 watch 等就绪后补一次
  • 不泄漏:回调里 stop() 并将 waitAccountStop = null,防一次性监听残留。

这块属于状态门控/就绪补偿,和防抖节流是两个维度的工程手段,组合后用户体验更稳。


常见坑 & 建议优化

  • ✅(可选)在 doFetch 执行后将 fetchSpecsTimeout = null,便于状态观察与调试。

    ts
    // 复杂逻辑:回调执行后清理句柄,便于判定当前是否有挂起的定时器
    fetchSpecsTimeout = setTimeout(async () => {
      try {
        await doFetch();
      } finally {
        // 复杂逻辑:回调已落地,句柄失效,置空
        fetchSpecsTimeout = null;
      }
    }, 100);
  • ✅(可选)在 await symbolInfoStore.fetchSpecs(...) 之后,再次校验“当前选中是否仍为该 tab”,避免慢请求的结果写回影响当前 UI(你现在用的是“开始前拦截”,必要时也可“结束后再校验”)。

  • ✅(可选)如果后续要取消在途请求,可在 fetchSpecs 中支持 AbortController 或内部比对最新 pinned 再落库。


如果用“库版防抖”,应如何等价表达?

等价语义是 尾沿防抖 + 账户就绪补偿 + 并发令牌。就算用 lodash.debounce,后两者依然要保留:

ts
// 复杂逻辑:创建尾沿防抖的 doFetch(等待 100ms)
const debouncedFetch = debounce(async (tab: SymbolTabItem, token: number) => {
  // 复杂逻辑:并发令牌拦截过期任务
  if (token !== lastFetchToken) return;

  if (!selectedAccount.value) {
    if (!waitAccountStop) {
      // 复杂逻辑:账户就绪后补一次拉取,并立刻注销监听
      const stop = watch(
        () => selectedAccount.value,
        async (acc) => {
          if (!acc) return;
          try {
            await symbolInfoStore.fetchSpecs([tab], acc);
          } finally {
            stop();
            waitAccountStop = null;
          }
        }
      );
      waitAccountStop = stop;
    }
    return;
  }

  await symbolInfoStore.fetchSpecs([tab], selectedAccount.value);
}, 100);

// 在 selectTab 内部调用时:
// 复杂逻辑:自增令牌,确保只保留“最后一次选择”的请求有效
const token = ++lastFetchToken;
debouncedFetch(tab, token);

可以看到:库只替换了“防抖外壳”token 和“就绪补偿”仍必须保留。


结论

  • 你的实现属于 尾沿防抖 的实际效果,但更强壮:通过 token 杀死过期回调,通过一次性 watch 在状态就绪后 可靠补一次
  • 和“我们平常理解的防抖”相比:多了并发安全与状态门控,适合交易、行情这类“高速切换、状态依赖强、避免脏写”的场景。

本站总访问